﻿#region File and License Information
/*
<File>
	<Copyright>Copyright © 2007, Daniel Vaughan. All rights reserved.</Copyright>
	<License see="prj:///Documentation/License.txt"/>
	<Owner Name="Daniel Vaughan" Email="dbvaughan@gmail.com"/>
	<CreationDate>2008-11-03 23:12:47Z</CreationDate>
	<LastSubmissionDate>$Date: $</LastSubmissionDate>
	<Version>$Revision: $</Version>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.Windows.Browser;

using DanielVaughan.Communication;

namespace DanielVaughan
{
	/// <summary>
	/// Singleton class that managers service channels and allows
	/// caching of channels for their reuse to improve performance.
	/// Default implementation of <see cref="IChannelManager"/>.
	/// </summary>
	public class ChannelManager : IChannelManager
	{
		string channelIdentifierField;
		readonly object channelIdentifierLock = new object();

		string ChannelIdentifier
		{
			get
			{
				if (channelIdentifierField == null)
				{
					lock (channelIdentifierLock)
					{
						if (channelIdentifierField == null)
						{
							UISynchronizationContext.Instance.InvokeSynchronously(delegate
				        		{
									try
									{
										channelIdentifierField = HtmlPage.Document.DocumentUri.ToString();
									}
									catch (Exception)
									{	/* Shouldn't get here. */
										channelIdentifierField = "ErrorRetrievingDocumentUri";
									}
				        		});
						}
					}
				}
				return channelIdentifierField;
			}
		}

		readonly Dictionary<Type, object> channels = new Dictionary<Type, object>();
		readonly object channelsLock = new object();

		public TChannel GetChannel<TChannel>()
		{
			Type serviceType = typeof(TChannel);
			object service;

			lock (channelsLock)
			{
				if (!channels.TryGetValue(serviceType, out service))
				{
					/* We don't cache the factory as it contains a list of channels 
					 * that aren't removed if a fault occurs. */
					var channelFactory = new ChannelFactory<TChannel>("*");

					service = channelFactory.CreateChannel();
					var communicationObject = (ICommunicationObject)service;
					communicationObject.Faulted += OnChannelFaulted;
					channels.Add(serviceType, service);
					communicationObject.Open(); /* Explicit opening of the channel 
												 * avoids a performance hit.  */
					ConnectIfClientService(service, serviceType);
				}
			}

			return (TChannel)service;
		}

		public TService GetDuplexChannel<TService>(object callbackInstance)
		{
			throw new System.NotImplementedException();
		}

		/// <summary>
		/// Called when a channel faults.
		/// Removes the channel from the cache so that it is 
		/// replaced when it is next required.
		/// </summary>
		/// <param name="sender">The sender.</param>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		void OnChannelFaulted(object sender, EventArgs e)
		{
			var communicationObject = (ICommunicationObject)sender;
			communicationObject.Faulted -= OnChannelFaulted;

			lock (channelsLock)
			{
				var keys = from pair in channels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					channels.Remove(key);
				}
			}
		}

		/// <summary>
		/// Attempts to excercise the <code>IServiceContract.InitiateConnection</code> method
		/// on the specified service if that service is an <code>IServiceContract</code>.
		/// That is, if the service has a method with the signature InitiateConnection(string),
		/// it will be invoked in a functional manner.
		/// </summary>
		/// <param name="service">The service to attempt a connection.</param>
		/// <param name="serviceType">Type of the service for logging purposes.</param>
		/// <exception cref="TargetInvocationException">Occurs if the service implements <code>IServiceContract</code>, 
		/// and the call to ConnectFromClient results in a <code>TargetInvocationException</code></exception>
		void ConnectIfClientService(object service, Type serviceType)
		{
			var beginMethodInfo = serviceType.GetMethod("BeginInitiateConnection");
			if (beginMethodInfo == null)
			{
				return;
			}

			beginMethodInfo.Invoke(service, new object[] { ChannelIdentifier, new AsyncCallback(ar =>
               	{
					var endMethodInfo = serviceType.GetMethod("EndInitiateConnection");
					if (endMethodInfo == null)
					{
						return;
					}
					try
					{
						var result = (string)endMethodInfo.Invoke(service, new object[] {ar});
						Debug.WriteLine("Connected from client successfully. Result: " + result);
						/* TODO: Do something with the result such as log the it somewhere. */
					}
					catch (InvalidCastException)
					{
						/* TODO: log that web server has invalid ConnectFromClient signature. */
					}
               	}), null });
		}

		#region Singleton implementation

		ChannelManager()
		{
			/* Intentionally left blank. */
		}

		public static ChannelManager Instance
		{
			get
			{
				return Nested.instance;
			}
		}

		class Nested
		{
			/* Explicit static constructor to tell C# compiler 
			 * not to mark type as beforefieldinit. */
			static Nested()
			{
			}

			internal static readonly ChannelManager instance = new ChannelManager();
		}

		#endregion
	}
}
